Linux Page Cache(草稿)

参考 SRE deep dive into Linux Page Cachemm/workingset.cio_uring

环境准备

安装依赖,下载内核源码,编译、安装 page-types 工具,生成测试数据文件,同步、清空缓存。

1
$ sudo apt install git build-essential golang vmtouch
1
2
$ uname -r
6.2.0-36-generic
1
2
3
4
5
6
7
$ mkdir kernel
$ cd kernel
$ wget https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/snapshot/linux-6.2.tar.gz
$ tar -xzf linux-6.2.tar.gz
$ cd linux-6.2/tools/vm
$ make
$ sudo make install
1
$ dd if=/dev/random of=/var/tmp/file1.db count=128 bs=1M
1
$ sync; echo 3 | sudo tee /proc/sys/vm/drop_caches

基本概念

基本操作

File reads

使用 Python 代码从文件读取 2B 数据,发现实际会读取 16 KB 的数据到缓存,操作系统会预读多个页面。使用 posix_fadvise() 提示内核,文件是随机访问的,此时内核不会使用预读优化。实测使用 mmap 系统调用,将文件映射到进程的虚拟内存,依然读取 2B 数据,此时内核预读 32 页而不是 read 系统调用的 4 页。

1
2
with open("/var/tmp/file1.db", "br") as f:
print(f.read(2))
1
2
3
4
5
6
$ strace -s0 python3 ./read_2_bytes.py
...
openat(AT_FDCWD, "/var/tmp/file1.db", O_RDONLY|O_CLOEXEC) = 3
...
read(3, ""..., 4096) = 4096
...
1
2
3
4
5
$ vmtouch /var/tmp/file1.db
Files: 1
Directories: 0
Resident Pages: 4/32768 16K/128M 0.0122%
Elapsed: 0.001331 seconds
1
2
3
4
5
6
import os

with open("/var/tmp/file1.db", "br") as f:
fd = f.fileno()
os.posix_fadvise(fd, 0, os.fstat(fd).st_size, os.POSIX_FADV_RANDOM)
print(f.read(2))
1
$ echo 3 | sudo tee /proc/sys/vm/drop_caches && python3 ./read_2_random.py
1
2
3
4
5
$ vmtouch /var/tmp/file1.db
Files: 1
Directories: 0
Resident Pages: 1/32768 4K/128M 0.00305%
Elapsed: 0.001301 seconds

File writes

更新文件的前 2B,发现内核读取 1 页数据到缓存。如果及时查看当前 cgroup 的脏页大小,会发现有 4KB 的数据还未刷盘。也可以使用 cat /proc/meminfo | grep Dirty 查看整个系统中的脏页大小,但是很难利用该信息。

1
2
with open("/var/tmp/file1.db", "br+") as f:
print(f.write(b"ab"))
1
$ sync; echo 3 | sudo tee /proc/sys/vm/drop_caches && python3 ./write_2_bytes.py
1
2
3
4
5
$ vmtouch /var/tmp/file1.db
Files: 1
Directories: 0
Resident Pages: 1/32768 4K/128M 0.00305%
Elapsed: 0.001764 seconds
1
2
3
4
$ cat /proc/self/cgroup
0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.gnome.Terminal.slice/vte-spawn-7fe5140d-9b95-4aff-9979-de88b1c42b94.scope
$ grep dirty /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.gnome.Terminal.slice/vte-spawn-7fe5140d-9b95-4aff-9979-de88b1c42b94.scope/memory.stat
file_dirty 4096
1
2
3
4
5
6
7
8
$ sudo page-types -f /var/tmp/file1.db -b dirty
/var/tmp/file1.db Inode: 402310 Size: 134217728 (32768 pages)
Modify: Mon Oct 13 19:35:25 2025 (2 seconds ago)
Access: Mon Oct 13 18:21:11 2025 (4456 seconds ago)

flags page-count MB symbolic-flags long-symbolic-flags
0x0000000000000038 1 0 ___UDl_______________________________________ uptodate,dirty,lru
total 1 0

缓存淘汰

每个 cgroup 都有一对活跃和非活跃列表,一对用于匿名页面,另一对用于文件页面。简单来说,发生缺页的页面被插入到非活跃列表,在非活跃列表被多次访问的页面升级到活跃列表。使用 LRU 算法结合 referenced 标志,控制页面的升级、降级和淘汰。referenced 被置位的页面,相当于在降级/淘汰时可以复活到当前列表的头部。此外,页缓存还会利用 shadow entry 计算 refault distance,从而减少非活跃列表空间不足导致的内存抖动问题。如果系统有 NUMA 节点,则会为每个节点维护列表,以减少锁竞争。

作者

Ligh0x74

发布于

2025-10-13

更新于

2025-10-13

许可协议

评论